<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>GPT眼中的dota2英雄颜值</title>
    <link href="https://hoppinzq.com/dota2/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://hoppinzq.com/dota2/js/bootstrap.bundle.min.js"></script>
</head>

<style>
    body {
        font-family: Poppins;
        background: linear-gradient(to left, #dad299, #b0dab9);
    }

    .hr-line {
        border-top: 1px solid transparent;
        background: linear-gradient(white, white) padding-box,linear-gradient(90deg, White, DimGrey, White) border-box;
    }

    .custom-modal {
        transition: all 0.3s ease-in-out;
    }

    .modal-backdrop {
        opacity: 0 !important;
    }

    #modalLol {
        background: linear-gradient(45deg, LightSalmon, FloralWhite);
    }

    #modalLol .modal-dialog {
        box-shadow: 0px 10px 20px #b33300;
    }

    #modalDota2 {
        background: linear-gradient(45deg, Crimson, FloralWhite);
    }

    #modalDota2 .modal-dialog {
        box-shadow: 0px 10px 20px #5d091a;
    }

    #modalLGpt {
        background: linear-gradient(45deg, CornflowerBlue, FloralWhite);
    }

    #modalLGpt .modal-dialog {
        box-shadow: 0px 10px 20px #103a89;
    }

    .custom-modal .modal-dialog {
        background-color: Whitesmoke;
        border: 2px dashed DimGrey;
        outline: 8px solid White;
        border-radius: 20px;
    }

    .custom-modal .modal-header {
        border-bottom: 2px dashed DimGrey;
    }

    .custom-modal .modal-title {
        width: 100%;
    }

    .custom-modal .modal-content {
        background-color: Whitesmoke;
        border: none;
        border-radius: 18px;
    }

    .custom-modal .modal-footer {
        border-top: 2px dashed DimGrey;
    }

    .lottie_container {
        width: 200px;
        margin: 0 auto;
    }

    dotlottie-player {
        border: 8px solid white;
        border-radius: 50%;
        overflow: hidden;
        box-shadow: 2px 2px 10px LightGrey inset;
    }

    .modal-button {
        width: 200px;
        height: 60px;
        font-family: Poppins;
        font-weight: 600;
        color: DimGrey;
        border: 4px solid White;
        background-color: WhiteSmoke;
        transition: all 0.2s ease-in;
    }

    .close-button {
        width: 40px;
        height: 40px;
        padding: 0;
        margin: 0;
        color: WhiteSmoke;
        border: 0px solid WhiteSmoke;
        border-radius: 50px;
        transition: all 0.2s ease-in;
    }

    .blue-btn:hover {
        box-shadow: 5px 5px rgba(30, 144, 255, 0.4), 10px 10px rgba(30, 144, 255, 0.3),
        15px 15px rgba(30, 144, 255, 0.2), 0px 0px 12px lightgrey;
        color: DodgerBlue;
    }

    .orange-btn:hover {
        box-shadow: 5px 5px rgba(255, 140, 0, 0.4), 10px 10px rgba(255, 140, 0, 0.3),
        15px 15px rgba(255, 140, 0, 0.2), 0px 0px 12px lightgrey;
        color: DarkOrange;
    }

    .red-btn:hover {
        box-shadow: 5px 5px rgba(255, 0, 0, 0.4), 10px 10px rgba(255, 0, 0, 0.3),
        15px 15px rgba(255, 0, 0, 0.2), 0px 0px 12px lightgrey;
        color: Red;
    }

    .color-1 {
        color: DodgerBlue;
    }

    .bg-color-1 {
        background-color: DodgerBlue;
    }

    .color-2 {
        color: DarkOrange;
    }

    .bg-color-2 {
        background-color: DarkOrange;
    }

    .color-3 {
        color: Crimson;
    }

    .bg-color-3 {
        background-color: Crimson;
    }

    .animate {
        transform-origin: 50% 100%;
        animation: animate 0.5s ease-in-out;
    }

    @keyframes animate {
        from {
            transform: scale(0);
            opacity: 0;
        }
        to {
            transform: scale(1);
            opacity: 100%;
        }
    }
    .modal.show {
        display: flex !important;
    }

    .modal.show .modal-dialog {
        margin: 0px 25px;
    }
    .modal-dota2{
        left: 100%;
    }
    .modal-lol{
        left: 100%;
    }
    .modal-gpt{
        left: 25%;
        width: 50%;
    }
    .modal-content{
        width: 322px;
    }
    .box-img {
        z-index: 1;
        position: absolute;
        width: 100%;
        height: 100%;
        object-fit: cover;
        opacity: 0.2;
        top: 0;
    }
    .cover-bg-img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        object-position: center;
    }
    .description{
        font-weight: bold;
    }

    .wyy-album-border {
        display: flex;
        align-items: center;
        position: relative;
        text-align: center;
        box-sizing: border-box;
        background-clip: padding-box;
        border: solid 8px transparent;
    }

    .wyy-album-border img {
        border-radius: 100%;
        width: 120px;
        height: 120px;
        margin: 0px auto;
    }

    .wyy-album-border:before {
        content: '';
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        z-index: -1;
        margin: -10px;
        border-radius: 100%;
        background: linear-gradient(315deg, rgba(10,10,10,0.8) 0%, rgba(36,43,48,0.8) 100%);
        transition: all 0.4s ease-in-out;
    }


    .wyy-flip-card {
        background-color: transparent;
        width: 150px;
        height: 150px;
        border-radius: 100%;
        perspective: 1000px;
        margin: 0px auto;
    }

    .wyy-flip-card-inner {
        position: relative;
        width: 150px;
        height: 150px;
        text-align: center;
        transition: transform 0.8s;
        transform-style: preserve-3d;
        margin: 0px auto;
    }

    .wyy-flip-card-front{
        position: absolute;
        width: 150px;
        height: 150px;
        backface-visibility: hidden;
    }

</style>

<body>
<h4 class="fw-bold text-center mt-4"><i class="fa-solid fa-layer-group fa-lg mt-5 me-2"></i>GPT4图像打分</h4>
<div class="hr-line mt-5 mb-5"></div>
<p class="fw-bold text-center text-muted mb-4">重要：1、前端页面是https的，后端接口暂时还不是https的，因此打分目前不可用；2、lol图片识别效果极差，先看个乐子，后续会更新。</p>
<div class="d-flex flex-wrap gap-4 justify-content-center">
    <button type="button" class="modal-button blue-btn p-3 ps-4 pe-4 gpt-btn">
        图片打分 Try！
    </button>
    <button type="button" class="modal-button orange-btn p-3 ps-4 pe-4 lol-btn">
        英雄联盟
    </button>
    <button type="button" class="modal-button red-btn p-3 ps-4 pe-4 dota2-btn">
        DOTA2
    </button>
</div>

<div class="modal fade custom-modal p-3" id="modalLol" data-bs-backdrop="static" tabindex="-1" data-bs-keyboard="false"
     aria-labelledby="Custom Modal" aria-hidden="true">
    <div class="modal-dialog modal-lol"></div>
</div>

<div class="modal fade custom-modal p-3" id="modalDota2" data-bs-backdrop="static" tabindex="-1" data-bs-keyboard="false"
     aria-labelledby="Custom Modal" aria-hidden="true">
    <div class="modal-dialog modal-dota2"></div>
</div>

<div class="modal fade custom-modal p-3" id="modalLGpt" data-bs-backdrop="static" data-bs-keyboard="false"
     tabindex="-1" aria-labelledby="Custom Modal" aria-hidden="true">
    <div class="modal-dialog modal-gpt">
        <div class="modal-content" style="width: 100%">
            <div class="modal-header">
                <h4 class="modal-title text-center fw-bold color-3">接口暂时不可用</h4>
            </div>
            <div class="modal-body">
                <p class="p-4 m-0 fw-bold">
                    <pre class="gpt-prompt"></pre>
                </p>
            </div>
            <div class="modal-footer justify-content-center p-3">
                <button type="button" class="close-button bg-color-3" data-bs-dismiss="modal" aria-label="Close Modal">
                    x</button>
            </div>
        </div>
    </div>
</div>

<div class="hr-line mt-5 mb-5"></div>

<p class="footer mt-4 fs-6 fw-bold text-center text-muted">特别鸣谢:</p>
<p class="text-center">
    <a title="没猫子" aria-label="More information about 没猫子" href="https://hoppinzq.com" target="_blank">
        <img src="https://hoppinzq.com/dota2/gpt/image/mm.jpg" alt="没猫子" width="40" height="40">
        <img src="https://hoppinzq.com/dota2/gpt/image/8d31ca64922761a76d07f26c9403d586.jpeg" alt="openAI" width="40" height="40">
    </a>
    <p class="text-center">
        感谢mm的提供的数据、样本和系统提示词，你的耐心和不厌其烦让我感激不已；
        感谢openAI提供的接口，你是一个令人吃惊的应用。
    </p>
</p>
<script src="https://hoppinzq.com/dota2/js/jquery.min.js"></script>
<script src="https://hoppinzq.com/dota2/js/anime.min.js"></script>
<script src="https://hoppinzq.com/static/js/plugins/localforage.js"></script>

<script>
    console.log("\n %c hoppinzq开源 %c https://gitee.com/hoppin \n\n", "background: #35495e; padding: 1px; border-radius: 3px 0 0 3px; color: #fff", "background: #fadfa3; padding: 1px; border-radius: 0 3px 3px 0; color: #fff");
    var ip = `http://localhost:8094`
    var webMetaData={
        dota2:{
            complete:false,                     //  dom装载是否完成，为true表示dota2的数据已经完全渲染成功
            cache_key:"__zq__dota2_gpt",        //  dota2数据缓存的key(数据缓存在localStorage)
            data:[],                            //  dota2数据
            animation:null,                     //  动画对象
            animation_settings:                 //  动画设置
            {
                targets: '.dota2',              //  动画作用的元素
                translateX: -350*124,           //  沿X轴移动的像素，负数表示向左移动
                translateY: 0,                  //  沿Y轴移动的像素
                delay: 1000,                    //  动画延迟$毫秒后播放
                direction: 'normal',            //  运动方向，正向normal/逆向reverse
                loop: false,                    //  是否循环播放动画
                easing: 'linear',               //  动画运动函数，默认线性，即匀速运动
                autoplay: false,                //  动画是否自动开始，默认不自动开始，开始动作由函数触发。一般需要监听dota2.complete是否为true，保证所有dom成功渲染，才开始
                duration: 2500*124              //  动画持续时间
            },
            modal:null                          //  弹框对象，可以监听弹框行为，以开始/停止动画
        },
        img:[],                                 //  图片缓存对象,内有属性:id 图片要放置的dom元素的id,key(图片缓存的key,图片blob对象缓存在indexDB),image(组装图片的dom),is_cache(是否命中图片缓存),url(图片url)
        lol:{
            complete:false,
            cache_key:"__zq__lol_gpt",
            data:[],
            img:[],
            animation:null,
            animation_settings:
                {
                    targets: '.lol',
                    translateX: -350*176,
                    delay: 1000,
                    direction: 'normal',
                    loop: false,
                    easing: 'linear',
                    autoplay: false,
                    duration: 2500*176
                },
            modal:null
        },
        gpt:{
            modal:"gpt-4o",
            default_prompt:"\n" +
                "你是一个审美机器人，每次对话我都给你一个图片链接，内容是一个游戏的英雄，大多不是人类的形象。请从以下几个方面对图片内的英雄进行评分：\n" +
                "1. **整体造型**: 英雄的整体设计和视觉效果。\n" +
                "2. **细节处理**: 细节部分的精细程度和美感。\n" +
                "3. **颜色搭配**: 颜色的搭配是否和谐、美观。\n" +
                "4. **创意性**: 设计的独特性和创意。\n" +
                "请分别给出每个方面的评分（最难看是0，最好看是100.0，分数精确到小数点一位），并计算出综合评分。最后，请以下面的JSON格式输出：\n" +
                "{\n" +
                "  \"url\": \"${图片链接}\",\n" +
                "  \"overall_score\": \"${综合分数}\",\n" +
                "  \"details\": {\n" +
                "    \"overall_design\": \"${整体造型分数}\",\n" +
                "    \"detail_handling\": \"${细节处理分数}\",\n" +
                "    \"color_matching\": \"${颜色搭配分数}\",\n" +
                "    \"creativity\": \"${创意性分数}\"\n" +
                "  },\n" +
                "  \"description\": \"${你对图片的评价}\"\n" +
                "}",
        }
    }
    $.fn.append2 = function(html, callback) {
        let originalHtmlLength = $("body").html().length;
        this.append(html);
        let nums = 1;
        let timer1 = setInterval(function() {
            nums++;
            let clearIntervalfun = function() {
                clearInterval(timer1)
                callback();
            }
            let flag = originalHtmlLength != $("body").html().length || nums > 1000;
            flag && clearIntervalfun()
        }, 1)
    };

    $(function () {
        $(".dota2-btn").click(function () {
            //  如果dota2已经打开过，则直接打开弹框，并延迟webMetaData.dota2.animation_settings.delay时间后，播放动画
            if(webMetaData.dota2.modal){
                webMetaData.dota2.modal.show();
                setTimeout(function () {
                    webMetaData.dota2.animation.play();
                }, webMetaData.dota2.animation_settings.delay)
                return;
            }
            //  如果dota2没有打开过，先渲染数据和dom
            let dota2Modal = new bootstrap.Modal(document.getElementById("modalDota2"), {});
            dota2Modal.show();
            webMetaData.dota2.modal=dota2Modal;
            //  从缓存中取数据，缓存不存在则请求json并设置缓存
            let dota2_data=window.localStorage.getItem(webMetaData.dota2.cache_key);
            if(dota2_data==null){
                $.getJSON("dota2.json",function (json) {
                    window.localStorage.setItem(webMetaData.dota2.cache_key,JSON.stringify(json));
                    loadDota2Dom(json);
                })
            }else{
                try{
                    loadDota2Dom(JSON.parse(dota2_data));
                }catch (e) {
                    $.getJSON("dota2.json",function (json) {
                        window.localStorage.setItem(webMetaData.dota2.cache_key,JSON.stringify(json));
                        loadDota2Dom(json);
                    })
                }
            }
            loadDota2Anime();
        })
        $(".lol-btn").click(function () {
            if(webMetaData.lol.modal){
                webMetaData.lol.modal.show();
                setTimeout(function () {
                    webMetaData.lol.animation.play();
                }, webMetaData.lol.animation_settings.delay)
                return;
            }
            let lolModal = new bootstrap.Modal(document.getElementById("modalLol"), {});
            lolModal.show();
            webMetaData.lol.modal=lolModal;
            //  从缓存中取数据，缓存不存在则请求json并设置缓存
            let lol_data=window.localStorage.getItem(webMetaData.lol.cache_key);
            if(lol_data==null){
                $.getJSON("lol_data.json",function (json) {
                    window.localStorage.setItem(webMetaData.lol.cache_key,JSON.stringify(json));
                    loadLolDom(json);
                })
            }else{
                try{
                    loadLolDom(JSON.parse(lol_data));
                }catch (e) {
                    $.getJSON("lol_data.json",function (json) {
                        window.localStorage.setItem(webMetaData.lol.cache_key,JSON.stringify(json));
                        loadLolDom(json);
                    })
                }
            }
            loadLolAnime();
        })
        $(".gpt-btn").click(function () {
            let lolModal = new bootstrap.Modal(document.getElementById("modalLGpt"), {});
            lolModal.show();
            $(".gpt-prompt").append(webMetaData.gpt.default_prompt)
        })
        
        
        $("#modalDota2").click(function () {
            webMetaData.dota2.modal.hide();
            webMetaData.dota2.animation.pause();
        })

        $("#modalLol").click(function () {
            webMetaData.lol.modal.hide();
            webMetaData.lol.animation.pause();
        })

        $("#start").click(function () {
            answer();
        })

        $("#stop").off("click").on("click",function () {
            stopChat();
        })

    })

    /**
     * 装配dota2的dom
     * @param json
     */
    function loadDota2Dom(json) {
        //  先初始化
        $("#modalDota2").html("");
        webMetaData.dota2.data=json;
        //  动态装配dom
        $.each(json,function (index,dota2_data) {
            $("#modalDota2").append2(`<div class="modal-dialog dota2 modal-dota2" data-id="${dota2_data.id}">
                    <div class="box-img over-hidden" id="image-dota2-1-${index}"></div>
                    <div class="modal-content">
                        <div class="modal-header">
                            <h3 class="modal-title text-center fw-bold color-1">${dota2_data.name}</h3>
                        </div>
                        <div class="modal-body">
                            <div class="lottie_container">
                                <div class="wyy-flip-card">
                                    <div class="wyy-flip-card-inner">
                                        <div class="wyy-album-border wyy-flip-card-front" id="image-dota2-2-${index}"></div>
                                    </div>
                                </div>
                            </div>
                            <p class="p-4 m-0 fw-bold">${dota2_data.dota2.description1}</p>
                        </div>
                        <div class="modal-footer justify-content-center p-3">
                            <h3 class="modal-title text-center fw-bold color-1">得分：${dota2_data.score}</h3>
                        </div>
                    </div>
                </div>`,function () {
                loadImage(`image-dota2-1-${index}`,`https://hoppinzq.com/dota2/png/${dota2_data.npc}.png`,"cover-bg-img","");
                loadImage(`image-dota2-2-${index}`,dota2_data.dota2.url);
                if(index==json.length-1){
                    //  装配完成，更新状态
                    webMetaData.dota2.complete=true;
                }
            });
        })

    }

    /**
     * 加载dota2动画
     */
    function loadDota2Anime() {
        //  监听状态，如果dota2装配完成，停止监听，并根据配置项装配动画
        let t=setInterval(function () {
            if(webMetaData.dota2.complete){
                clearInterval(t);
                let animation = anime(webMetaData.dota2.animation_settings);
                animation.play();
                webMetaData.dota2.animation=animation;
            }
        },100)
    }

    function loadLolDom(json){
        $("#modalLol").html("");
        webMetaData.lol.data=json;
        //  动态装配dom
        $.each(json,function (index,lol_data) {
            $("#modalLol").append2(`<div class="modal-dialog lol modal-lol" data-id="${lol_data.hero_id}">
                    <div class="box-img over-hidden" id="image-lol-1-${index}" style="opacity: 0.35"></div>
                    <div class="modal-content">
                        <div class="modal-header">
                            <h3 class="modal-title text-center fw-bold color-1">${lol_data.hero_name}</h3>
                        </div>
                        <div class="modal-body">
                            <p class="p-4 m-0 lol-description fw-bold">${lol_data.hero_description1}</p>
                        </div>
                        <div class="modal-footer justify-content-center p-3">
                            <h3 class="modal-title text-center fw-bold color-1">得分：${lol_data.score}</h3>
                        </div>
                    </div>
                </div>`,function () {
                loadImage(`image-lol-1-${index}`,`${lol_data.hero_image}`,"cover-bg-img","");
                if(index==json.length-1){
                    webMetaData.lol.complete=true;
                }
            });
        })
    }

    function loadLolAnime() {
        let t=setInterval(function () {
            if(webMetaData.lol.complete){
                clearInterval(t);
                let animation = anime(webMetaData.lol.animation_settings);
                animation.play();
                webMetaData.lol.animation=animation;
            }
        },100)
    }

    /**
     * 加载图片，先读缓存，缓存图片的blob对象
     * @param url   图片url
     * @param className 图片img标签的class元素
     * @param alt   图片标签的alt元素
     * @param style 图片标签自定义样式
     */
    function loadImage(id,url, className, alt, style) {
        //  将图片url转base64，作为图片id和缓存的key
        let encodeUrl = window.btoa(url);
        let key = encodeUrl;
        //  动态装配img标签
        let image = new Image();
        $(image).addClass("image-" + id).addClass(className).attr("alt", alt);
        if (style) {
            for (let cssKey in style) {
                $(image).css(cssKey, style[cssKey]);
            }
        }
        //  从indexDB取图片缓存，若存在，生成一个url对象指向缓存的图片blob对象；若不存在，先请求图片，转blob存indexDB，然后使用原图片url对象
        localforage.getItem(encodeUrl, function (err, value_) {
            if (value_ != null) {
                url = window.URL.createObjectURL(value_);
                image.src = url;
                $(`#${id}`).append(image.outerHTML);
                webMetaData.img.push({
                    id:id,
                    key: key,
                    is_cache: true,
                    url: url,
                    image:image
                })
            } else {
                image.src = url;
                $(`#${id}`).append(image.outerHTML);
                webMetaData.img.push({
                    id:id,
                    key: key,
                    is_cache: false,
                    url: url,
                    image:image
                })
                let xhr = new XMLHttpRequest();
                let blob;
                xhr.open('GET', url, true);
                xhr.responseType = 'blob';
                xhr.onload = function () {
                    let data = xhr.response;
                    blob = new Blob([data]);
                    localforage.setItem(encodeUrl, blob);
                };
                xhr.send();
            }
        });
    }

    /**
     * 停止对话
     */
    function stopChat() {
        webMetaData.sse.close();
    }

    /**
     * 开始对话
     */
    function answer() {
        let eventSource = new EventSource(ip + '/image?message=https://hoppinzq.oss-cn-hangzhou.aliyuncs.com/game/game_7.jpg');
        webMetaData.sse = eventSource;
        eventSource.onopen = function (event) {
        };
        eventSource.addEventListener("message", function (event) {
            let msg = event.data;
            let chat = JSON.parse(msg);
            if (chat.choices[0].message.content != undefined && chat.choices[0].message.content != null) {
                webMetaData.chatState = 2;
                $("#test").text($("#test").text() + chat.choices[0].message.content);
            }
        });
        eventSource.addEventListener("error", function (event) {
            eventSource.close();
        });

    }

</script>
</body>
</html>
